package t4tools.splittingObjects.splitting;

import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.jxpath.JXPathContext;

import t4tools.splittingObjects.config.CompoundPropertyColumnConfig;
import t4tools.splittingObjects.config.RelationConfig;
import t4tools.splittingObjects.filter.RowFilter;
import t4tools.splittingObjects.model.Cell;
import t4tools.splittingObjects.model.CellImpl;
import t4tools.splittingObjects.model.CompoundPropertyCell;
import t4tools.splittingObjects.model.Row;
import t4tools.splittingObjects.model.RowImpl;
import t4tools.splittingObjects.order.Order;

/**
 * Handles the splitting of an object into rows.
 * 
 * @author yasko
 * 
 * @param <T>
 *            Type of the root object.
 */
public class Splitter {

	private int iterationIndex = -1;

	private JXPathContext jxPathContext;

	private RowConfig config;

	private Set<String> addedNonRepeatableCells = new HashSet<String>();

	private Map<String, Cell> repeatedCellsCache = new HashMap<String, Cell>();

	private Set<Row> rows;

	private Splitter(JXPathContext context, RowConfig config) {
		this.jxPathContext = context;
		this.config = config;
	}

	public static Splitter newInstance(RowConfig config, Object object) {
		return new Splitter(JXPathContext.newContext(object), config);
	}

	public boolean hasNext() {
		return false;
	}

	public Set<Row> build() {
		if (rows == null) {
			Comparator<Row> comparator = (config.getOrder() != null) ? config.getOrder() : Order.DEFAULT;
			rows = new TreeSet<Row>(comparator);
		} else {
			rows.clear();
		}
		while (true) {
			iterationIndex++;
			Row row = new RowImpl();
			addRepeatedCells(row);
			addNonRepeatedSimpleCells(row);
			addCompundCells(row);
			if (!addNextPureArrayCells(row)) {
				break;
			}
			if (applyRowFilters(row)) {
				rows.add(row);
			}
		}
		return rows;
	}

	private void addRepeatedCells(Row addTo) {
		for (String path : config.getRepeatedSimpleColumns()) {
			if (addTo.getCells().containsKey(path)) {
				continue;
			}
			addRepeatedCell(path, addTo);
		}
	}

	private void addNonRepeatedSimpleCells(Row addTo) {
		for (String path : config.getNonRepeatedSimpleColumns()) {
			// TODO: cell instances should be cached
			if (addTo.getCells().containsKey(path)) {
				continue;
			}
			addNonRepeatedCell(path, addTo);
		}
	}

	private boolean addNextPureArrayCells(Row addTo) {
		boolean hasMoreRows = false;
		for (String path : config.getPureArrayColumns()) {
			// TODO: cell instances should be cached
			if (addTo.getCells().containsKey(path)) {
				continue;
			}
			hasMoreRows |= addPureArrayCell(addTo, path);
		}
		return hasMoreRows;
	}

	public void addCompundCells(Row addTo) {
		for (CompoundPropertyColumnConfig iterable : config.getCompoundPropertyColumns()) {
			String key = iterable.getKey();
			if (addTo.getCells().containsKey(key)) {
				continue;
			}
			if (!iterable.isRepeatable() && addedNonRepeatableCells.contains(key)) {
				continue;
			}
			CompoundPropertyCell cell = null;
			if (iterable.isRepeatable()) {
				if (repeatedCellsCache.containsKey(key)) {
					cell = (CompoundPropertyCell) repeatedCellsCache.get(key);
				} else {
					cell = new CompoundPropertyCell();
					repeatedCellsCache.put(key, cell);
				}
			} else {
				cell = new CompoundPropertyCell();
			}
			cell.setKey(iterable.getKey());
			for (String path : iterable.getProperties()) {
				Object value = jxPathContext.getValue(path);
				cell.getPropertiesMap().put(path, value);
			}
			addTo.addCell(cell);
		}
	}

	private boolean retrieveAndApplyRelation(Cell inRelation, Row row) {
		RelationConfig relationConfig = config.getRelations().get(inRelation.getKey());
		if (relationConfig != null) {
			Map<String, Cell> relatedCellsMap = new HashMap<String, Cell>();
			for (String relatedPath : relationConfig.getRelatedPaths()) {
				if (!row.getCells().containsKey(relatedPath)) {
					if (!addedNonRepeatableCells.contains(relatedPath)
							&& config.getNonRepeatedSimpleColumns().contains(relatedPath)) {
						addNonRepeatedCell(relatedPath, row);
					} else if (config.getRepeatedSimpleColumns().contains(relatedPath)) {
						addRepeatedCell(relatedPath, row);
					} else if (config.getPureArrayColumns().contains(relatedPath)) {
						addPureArrayCell(row, relatedPath);
					}
				}
				Cell cell = row.getCell(relatedPath);
				relatedCellsMap.put(relatedPath, cell);
			}
			return relationConfig.getRelation().relates(inRelation, relatedCellsMap);
		}
		return true;
	}

	private void addRepeatedCell(String path, Row row) {
		Cell cell = repeatedCellsCache.get(path);
		if (cell == null) {
			cell = new CellImpl();
			repeatedCellsCache.put(path, cell);
			cell.setKey(path);
			cell.setValue(jxPathContext.getValue(path));
		}
		row.addCell(cell);
	}

	private void addNonRepeatedCell(String path, Row row) {
		if (!addedNonRepeatableCells.contains(path)) {
			Cell cell = new CellImpl();
			cell.setKey(path);
			cell.setValue(jxPathContext.getValue(path));
			if (retrieveAndApplyRelation(cell, row)) {
				row.addCell(cell);
				addedNonRepeatableCells.add(path);
			}
		}
	}

	private boolean addPureArrayCell(Row row, String path) {
		Cell cell = new CellImpl();
		cell.setKey(path);

		Iterator iterator = jxPathContext.iterate(path);
		Object cellValue = null;
		// FIXME: cartman says - this is bull crap
		int index = 0;
		boolean added = false;
		if (iterator.hasNext()) {
			do {
				Object next = iterator.next();
				if (index == iterationIndex) {
					cellValue = next;
					cell.setValue(cellValue);
					if (retrieveAndApplyRelation(cell, row)) {
						row.addCell(cell);
						added = true;
					}
					break;
				}
				index++;
			} while (iterator.hasNext());
		}
		return added;
	}

	private boolean applyRowFilters(Row row) {
		for (RowFilter filter : config.getRowFilters()) {
			if (!filter.apply(row)) {
				return false;
			}
		}
		return true;
	}

}
